package be.panako.cli;


import be.panako.strategy.QueryResult;
import be.panako.strategy.QueryResultHandler;
import be.panako.strategy.Strategy;
import be.panako.util.AudioFileUtils;
import be.panako.util.Config;
import be.panako.util.FileUtils;
import be.panako.util.Key;
import be.tarsos.dsp.io.PipeDecoder;
import be.tarsos.dsp.io.PipedAudioStream;
import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * A command line application to bulk load cached fingerprints.
 */
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal", "unused", "MismatchedQueryAndUpdateOfCollection"})
class Video extends Application {
    private static final Logger LOG = Logger.getLogger(Video.class.getName());
    private final List<Match> results = new ArrayList<>();
    @Parameter(names = "-log", converter = LevelConverter.class)
    private Level logLevel = Level.WARNING;
    @Parameter(names = "-mask", converter = PatternConverter.class)
    private Pattern nameMask;
    @Parameter(required = true)
    private List<String> names;
    @Parameter(names = "-out", required = true)
    private String outFilename;
    @Parameter(names = "-merge")
    private int mergeRange;
    @Parameter(names = "-skip-first")
    private int skipFirst;
    @Parameter(names = "-skip-last")
    private int skipLast;
    @Parameter(names = "-max")
    private int fileLimit = Integer.MAX_VALUE;
    private PipeDecoder pipeDecoder;

    @Override
    public void run(String... args) {
        try {
            JCommander.newBuilder()
                    .addObject(this)
                    .build()
                    .parse(args);

            LOG.info(() -> "USING strategy " + Config.get(Key.STRATEGY));
            Panako.setLevel(logLevel);

            List<String> files = new ArrayList<>();
            for (String fn : names) {
                if (Files.exists(Paths.get(fn)))
                    files.add(fn);
                else
                    LOG.severe(() -> "File not found - " + fn);
            }
            if (files.isEmpty()) {
                LOG.severe("No files to process");
                return;
            }

            String pipeEnvironment = Config.get(Key.DECODER_PIPE_ENVIRONMENT);
            String pipeArgument = Config.get(Key.DECODER_PIPE_ENVIRONMENT_ARG);
            String pipeCommand = Config.get(Key.DECODER_PIPE_COMMAND);
            String pipeLogFile = Config.get(Key.DECODER_PIPE_LOG_FILE);
            int pipeBuffer = Config.getInt(Key.DECODER_PIPE_BUFFER_SIZE);
            int cpus = be.panako.cli.Application.availableProcessors();
            ExecutorService runners = Executors.newFixedThreadPool(cpus);
            AudioFileUtils.OsCharset = Charset.forName("cp866");

            pipeDecoder = new PipeDecoder(pipeEnvironment, pipeArgument, pipeCommand, pipeLogFile, pipeBuffer);
            PipedAudioStream.setDecoder(pipeDecoder);

            for (int i = 0; i < Math.min(fileLimit, files.size()); i++) {
                String fn = files.get(i);
                String bn = FileUtils.basename(fn);
                if (nameMask != null) {
                    Matcher mat = nameMask.matcher(bn);
                    if (!mat.find()) {
                        LOG.warning(() -> "Skipping file " + fn);
                        continue;
                    }
                    bn = mat.group();
                }
                runners.submit(new Job(fn, bn));
            }
            runners.shutdown();
            //noinspection StatementWithEmptyBody
            while (!runners.awaitTermination(1, TimeUnit.DAYS)) ;
            if (outFilename != null)
                Match.writeTo(outFilename, results);
        } catch (Exception ex) {
            LOG.severe(ex.getMessage());
        }
    }

    void addResults(List<Match> matches) throws IOException {
        synchronized (results) {
            results.addAll(matches);
            if (outFilename != null)
                Match.writeTo(outFilename, results);
        }
    }

    @Override
    public String description() {
        return "scan videos";
    }

    @Override
    public String synopsis() {
        return "video";
    }

    @Override
    public boolean needsStorage() {
        return true;
    }

    @Override
    public boolean writesToStorage() {
        return false;
    }

    static class Match implements Comparable<Match> {
        static DateTimeFormatter dtfm = DateTimeFormatter.ofPattern("H:mm:ss");
        String filename;
        String matchName;
        double startsAt;
        double endsAt;

        public static String str(double time) {
            return LocalDateTime.ofEpochSecond((long) time, 0, ZoneOffset.UTC).format(dtfm);
        }

        public static double parse(String time) {
            LocalTime ldt = LocalTime.from(dtfm.parse(time));
            return ldt.toSecondOfDay();
        }

        public static void writeTo(String filename, List<Match> matches) throws IOException {
            Files.write(Paths.get(filename), matches.stream()
                    .map(Match::toString).sorted()
                    .toList());
        }

        public static Match of(String line) {
            String[] cells = line.split("\t");
            Match me = new Match();
            me.filename = cells[0];
            me.startsAt = parse(cells[1]);
            me.endsAt = parse(cells[2]);
            me.matchName = cells[4];
            return me;
        }

        public void pad(int padding) {
            startsAt = Math.max(0, startsAt - padding);
            endsAt += padding;
        }

        public String start() {
            return str(startsAt);
        }

        public String end() {
            return str(endsAt);
        }

        public String length() {
            return str(endsAt - startsAt);
        }

        @Override
        public String toString() {
            return String.format("%s\t%s\t%s\t%s\t%s",
                    filename, str(startsAt), str(endsAt), str(endsAt - startsAt), matchName);
        }

        @Override
        public int hashCode() {
            return toString().hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof Match m && compareTo(m) == 0;
        }

        @Override
        public int compareTo(Match o) {
            int res = o.matchName.compareTo(this.matchName);
            if (res == 0) {
                res = o.filename.compareTo(this.filename);
                if (res == 0)
                    res = Double.compare(o.startsAt, this.startsAt);
            }
            return -res;
        }
    }

    static class PatternConverter implements IStringConverter<Pattern> {
        @Override
        public Pattern convert(String s) {
            return Pattern.compile(s);
        }
    }

    static class LevelConverter implements IStringConverter<Level> {
        @Override
        public Level convert(String s) {
            return Level.parse(s);
        }
    }

    class Job implements Runnable, QueryResultHandler {
        private final String filenname;
        private final Strategy strategy;
        private final int numberOfQueryResults;
        private final List<Match> matches;
        private final String baseName;

        public Job(String filenname, String basename) {
            this.filenname = filenname;
            this.baseName = basename;
            this.strategy = Strategy.getInstance();
            this.numberOfQueryResults = Config.getInt(Key.NUMBER_OF_QUERY_RESULTS);
            this.matches = new ArrayList<>(numberOfQueryResults);
        }

        public String changeExtension(String fn, String newExtension) {
            int i = fn.lastIndexOf('.');
            return (i > 0) ? fn.substring(0, i) + newExtension : fn;
        }

        String threadIndex() {
            String name = Thread.currentThread().getName();
            return name.substring(name.lastIndexOf('-') + 1);
        }

        private void merge() {
            if (matches.size() < 2)
                return;

            int i = 1;
            Match prev = matches.get(0);
            while (i < matches.size()) {
                Match m = matches.get(i);
                boolean keep = true;
                if (prev.endsAt >= m.startsAt) {
                    if (m.endsAt > prev.endsAt)
                        prev.endsAt = m.endsAt;
                    matches.remove(i);
                } else if ((prev.endsAt + mergeRange >= m.startsAt) && prev.matchName.equals(m.matchName)) {
                    prev.endsAt = m.endsAt;
                    matches.remove(i);
                } else {
                    prev = m;
                    i++;
                }
            }
        }

        @Override
        public void run() {
            try {
                String prefix = String.format("[%s/%d]", threadIndex(), Application.availableProcessors());
                System.out.format("%s %TT Processing %s ...%n", prefix, LocalDateTime.now(), filenname);
                strategy.monitor(filenname, numberOfQueryResults, new HashSet<>(), this);

                String ttfn = changeExtension(filenname, ".tsv");
                Match.writeTo(ttfn, matches);
                merge();

                while (!matches.isEmpty() && (matches.get(0).startsAt < skipFirst))
                    matches.remove(0);

                if (skipLast > 0) {
                    double x = pipeDecoder.getDuration(filenname) - skipLast;
                    while (!matches.isEmpty()) {
                        int n = matches.size() - 1;
                        if (matches.get(n).endsAt < x)
                            break;
                        matches.remove(n);
                    }
                }

                addResults(matches);
                System.out.format("%s %TT %s: found %d matches, %d total%n",
                        prefix, LocalDateTime.now(), baseName, matches.size(), results.size());
            } catch (Throwable th) {
                LOG.severe(th.getMessage() + " for " + filenname);
            }
        }

        @Override
        public void handleQueryResult(QueryResult result) {
            if (result.refPath != null) {
                Match m = new Match();
                m.filename = baseName;
                m.matchName = FileUtils.basename(result.refPath);
                m.startsAt = result.queryStart;
                m.endsAt = result.queryStop;
                matches.add(m);
            }
        }

        @Override
        public void handleEmptyResult(QueryResult result) {
//           // nothing to do
        }
    }

}
